﻿//****************************************************************************
// Copyright (c) Microsoft Corporation. All rights reserved.
//****************************************************************************
using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.FxCop.Sdk;
using Microsoft.VisualStudio.CodeAnalysis.Extensibility;

namespace Microsoft.FxCop.Rules.ASP.MVC.Security
{
    
    sealed class MarkVerbHandlersWithValidateAntiforgeryToken : BaseIntrospectionRule
    {

        // copy of the MVC HttpVerbs enum
        private enum HttpVerbs
        {
            Get = 1,
            Post = 2,
            Delete = 8,
            Put = 4,
            Head = 16
        }

        private class EnabledHttpVerbs
        {
            public bool Get = false;
            public bool Post = false;
            public bool Delete = false;
            public bool Put = false;
            public bool Head = false;
        }

        #region *** Fields ***
        #endregion

        #region *** Constructor ***
        public MarkVerbHandlersWithValidateAntiforgeryToken()
            : base(typeof(MarkVerbHandlersWithValidateAntiforgeryToken).Name, "Microsoft.FxCop.Rules.ASP.MVC.Security.ASPMvcSecurityRules", typeof(MarkVerbHandlersWithValidateAntiforgeryToken).Assembly)
        {
        }
        #endregion

        #region *** Methods ***

        #region *** BaseIntrospectionRule ***

        public override TargetVisibilities TargetVisibility
        {
            get
            {
                return TargetVisibilities.ExternallyVisible | TargetVisibilities.NotExternallyVisible | TargetVisibilities.Overridable;
            }
        }

        public override ProblemCollection Check(Member member)
        {
            Method method;

            if( (method = (member as Method)) != null )
            {
                if( MVCHelper.IsControllerAction(method) )
                    AnalyzeMethodAttributes(method);
            }

            return base.Problems;
        }
        #endregion

        #region *** Private ***

        private void AnalyzeMethodAttributes(Method method)
        {
            EnabledHttpVerbs verbs;
            bool antiForgeryTokenDefined;
            bool verbDefined;

            verbDefined = RetreiveInfoFromMethodAttribute(method, out verbs, out antiForgeryTokenDefined);

            // Case 1 - If no attributes are specified
            // Example:
            //  public class ControllerMarkVerbHandlersWithDefaultVerbsAndNoToken : System.Web.Mvc.Controller
            //  {
            //    public ActionResult PostMethod(string data)
            //    {
            //        return null;
            //    }
            //  }
            if (!antiForgeryTokenDefined && !verbDefined)
            {
                ReportProblem(Localized.ValidateAntiforgeryTokenRule_Case1);
            }

            // Case 2 - ValidateAntiForgeryTokenAttribute specified with no AcceptVerb
            //Example:
            //public class ControllerMarkVerbHandlersWithDefaultVerbsAndToken : System.Web.Mvc.Controller
            //{
            //    [ValidateAntiForgeryToken()]
            //    public ActionResult PostMethod(string data)
            //    {
            //        return null;
            //    }
            //}
            else if (antiForgeryTokenDefined && !verbDefined)
            {
                ReportProblem(Localized.ValidateAntiforgeryTokenRule_Case2);
            }
            else  // verbs were defined
            {
                // Case 3 - POST and GET defined with ValidateAntiForgeryToken
                //Example
                //public class ControllerMarkVerbHandlersWithGetAndPostVerbsAndToken : System.Web.Mvc.Controller
                //{
                //    [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]
                //    [ValidateAntiForgeryToken()]
                //    public ActionResult PostMethod(string data)
                //    {
                //        return null;
                //    }
                //}
                //
                if (verbs.Get && verbs.Post && antiForgeryTokenDefined)
                {
                    ReportProblem(Localized.ValidateAntiforgeryTokenRule_GetAndPost);
                }

                // Case 4 - GET defined with ValidateAntiForgeryToken
                // Example:
                //public class ControllerMarkVerbHandlersWithGetVerbAndToken : System.Web.Mvc.Controller
                //{
                //    [AcceptVerbs(HttpVerbs.Get)]
                //    [ValidateAntiForgeryToken()]
                //    public ActionResult PostMethod(string data)
                //    {
                //        return null;
                //    }
                //}
                else if (verbs.Get && antiForgeryTokenDefined)
                {
                    ReportProblem(Localized.ValidateAntiforgeryTokenRule_GetWithToken, MessageLevel.Warning);
                }

                // Case 5 - Post defined without ValidateAntiForgeryToken
                // Example:
                //public class ControllerMarkVerbHandlersWithPostVerbAndMissingToken : System.Web.Mvc.Controller
                //{
                //    [AcceptVerbs(HttpVerbs.Post)]
                //    public ActionResult PostMethod( string data )
                //    {
                //        return null;
                //    }
                //}
                else if (verbs.Post && !antiForgeryTokenDefined)
                {
                    ReportProblem(Localized.ValidateAntiforgeryTokenRule_Case1);
                }
            }
        }

        private static bool RetreiveInfoFromMethodAttribute(Method method, out EnabledHttpVerbs verbs, out bool antiForgeryTokenDefined )
        {
            verbs = new EnabledHttpVerbs();
            antiForgeryTokenDefined = false;
            bool verbDefined = false;

            foreach (AttributeNode attrNode in method.Attributes)
            {
                switch (attrNode.Type.FullName)
                {
                    case "System.Web.Mvc.ValidateAntiForgeryTokenAttribute":
                        antiForgeryTokenDefined = true;
                        break;
                    case "System.Web.Mvc.HttpGetAttribute":
                        verbs.Get = true;
                        verbDefined = true;
                        break;
                    case "System.Web.Mvc.HttpPostAttribute":
                        verbs.Post = true;
                        verbDefined = true;
                        break;
                    case "System.Web.Mvc.HttpPutAttribute":
                        verbs.Put = true;
                        verbDefined = true;
                        break;
                    // No definition for System.Web.Mvc.HttpHeadAttribute
                    case "System.Web.Mvc.AcceptVerbsAttribute":
                        ProcessAcceptVerbsAttribute(attrNode, verbs);
                        verbDefined = true;
                        break;
                }
            }

            // If not verbs were defined, POST + GET are enabled
            if (!verbDefined)
            {
                verbs.Get = true;
                verbs.Post = true;
            }

            return verbDefined;
        }

        private static void ProcessAcceptVerbsAttribute(AttributeNode acceptVerbAttr, EnabledHttpVerbs verbs)
        {
            int verbValue;

            verbValue = GetVerbValueFromAcceptVerbsAttribute(acceptVerbAttr);

            verbs.Get = ((verbValue & (int)HttpVerbs.Get) == (int)HttpVerbs.Get);           // GET defined
            verbs.Post = ((verbValue & (int)HttpVerbs.Post) == (int)HttpVerbs.Post);        // POST defined
            verbs.Put = ((verbValue & (int)HttpVerbs.Put) == (int)HttpVerbs.Put);           // PUT defined
            verbs.Head = ((verbValue & (int)HttpVerbs.Head) == (int)HttpVerbs.Head);        // HEAD defined
            verbs.Delete = ((verbValue & (int)HttpVerbs.Delete) == (int)HttpVerbs.Delete);  // HEAD defined
        }

        private static int GetVerbValueFromAcceptVerbsAttribute(AttributeNode acceptVerbAttr)
        {
            Literal expLit;
            int verbValue = 0;
            string[] verbs;
            MarkVerbHandlersWithValidateAntiforgeryToken.HttpVerbs currentVerb;

            expLit = (Literal)acceptVerbAttr.Expressions[0];

            //AcceptVerbsAttribute(HttpVerbs) 
            if (expLit.Type.FullName == "System.Web.Mvc.HttpVerbs")
                verbValue = (int)expLit.Value;
            else //AcceptVerbsAttribute(String[])
            {
                verbs = (string[])expLit.Value;

                //convert the string array to HttpVerbs
                for (int i = 0; i < verbs.Length; i++)
                {
                    try
                    {
                        currentVerb = (HttpVerbs)Enum.Parse(typeof(HttpVerbs), verbs[i], true);
                    }
                    catch (ArgumentException)
                    {
                        continue;
                    }

                    if (i == 0)
                        verbValue = (int)currentVerb;
                    else
                        verbValue |= (int)currentVerb;
                }
            }

            return verbValue;
        }

        private void ReportProblem(string issueData)
        {
            Problem problem = new Problem(base.GetResolution(new string[] { issueData }));
            base.Problems.Add(problem);
        }

        private void ReportProblem(string issueData, MessageLevel messageLevel)
        {
            Problem problem = new Problem(base.GetResolution(new string[] { issueData }));

            problem.MessageLevel = messageLevel;
            base.Problems.Add(problem);
        }

        #endregion

        #endregion
    }
}
